library(tidyverse)
library(tidymodels)
library(tidyquant)
# remotes::install_github("business-science/timetk")
library(timetk)
Loading Stock Data with tidyquant
For our data, we’ll create a portfolio of 5 ETFs: SPY, EFA, IJS, EEM, and AGG.
First, we’ll load the data from Yahoo Finance using tq_get() and will calculate daily returns using tq_transmute().
# The symbols vector holds our tickers.
symbols <- c("SPY","EFA", "IJS", "EEM","AGG")
etf_daily_returns <-
symbols %>%
tq_get(get = "stock.prices", from = "2004-01-01") %>%
group_by(symbol) %>%
tq_transmute(adjusted, periodReturn, period = "daily", col_rename = "returns")
Next, we’ll build a portfolio with the following weights:
- SPY: 25%
- EFA 25%
- IJS 25%
- EEM 20%
- AGG 5%
w_agg <- c(0.25, 0.25, 0.25, 0.20, 0.05)
portfolio_returns <-
etf_daily_returns %>%
tq_portfolio(assets_col = symbol,
returns_col = returns,
weights = w_agg,
col_rename = "returns")
Visualize Time Series Data
We can create a nice looking plot of our time series data with a single line of code.
portfolio_returns %>%
plot_time_series(date, returns, .color_var = year(date),
.interactive = FALSE, .color_lab = "Year")

We can even make this plot interactive using timetk’s integration with plotly:
portfolio_returns %>%
plot_time_series(date, returns, .color_var = year(date),
.interactive = TRUE, .plotly_slider = TRUE, .color_lab = "Year")
timetk will automatically facet plots with grouped data:
etf_daily_returns %>%
group_by(symbol) %>%
plot_time_series(date, returns, .facet_ncol = 2, .color_var = symbol, .interactive = FALSE)

timetk also has a built-in function for plotting anomaly diagnostics:
portfolio_returns %>%
plot_anomaly_diagnostics(date, returns)
frequency = 5 observations per 1 week
trend = 64 observations per 3 months
To adjust outlier detection, change the alpha parameter.
Anomaly detection is done by the tk_anomaly_diagnostics function:
portfolio_returns %>%
tk_anomaly_diagnostics(date, returns)
frequency = 5 observations per 1 week
trend = 64 observations per 3 months
Shiny with timetk
We can use these visualization tools to easily build a simple Shiny application that helps us diagnose time series data.
Data Wrangling
Summarizing and filtering by time
timetk also includes some functions that work like common dplyr verbs, but are especially useful when working with time series data.
Let’s look at our individual ETFs to apply some of the data wrangling tools from timetk.
etf_daily_returns %>%
group_by(symbol) %>%
plot_time_series(date, returns, .facet_ncol = 2, .interactive = FALSE)

We can use summarise_by_time() to aggregate over a specific time period. For example, let’s smooth our returns by averaging them over each month:
etf_daily_returns %>%
summarise_by_time(
date, .by = "month",
returns = AVERAGE(returns)
) %>%
plot_time_series(date, returns, .facet_ncol = 2, .interactive = FALSE)

We can also filter by time range using the filter_by_time() function:
etf_daily_returns %>%
filter_by_time(date, "2012-06", "2020") %>%
summarise_by_time(
date, .by = "month",
returns = AVERAGE(returns)
) %>%
plot_time_series(date, returns, .facet_ncol = 2, .interactive = FALSE)

Padding and imputing missing data
timetk also has functions for padding and imputing missing data.
Our tibbles of data are missing some dates - weekends and holidays. In our ETF returns:
etf_daily_returns
Some packages for working with time series data require there to be no missing entries. Luckily, timetk makes it easy to add placeholders for any msising dates in our data.
The pad_by_time() function will replace missing values with NA by default.
etf_daily_returns %>%
pad_by_time(date, .by = "auto")
pad applied on the interval: day
pad applied on the interval: day
pad applied on the interval: day
pad applied on the interval: day
pad applied on the interval: day
In some cases, we may want to impute missing values (i.e., replace NA with an actual value). This also allows us to impute more granular data, like hourly stock prices.
Let’s start with the daily closing prices for SPY:
spy_prices <-
tq_get("SPY", get = "stock.prices", from = "2004-01-01")
spy_prices
spy_prices %>%
plot_time_series(date, close, .interactive = FALSE)

Now let’s impute hourly closing prices for every day of the year using ts_impute_vec(). (This is done linearly when period = 1)
spy_prices_hourly <-
spy_prices %>%
pad_by_time(date, .by = "hour") %>% # pad with additional observations for every hour
mutate_at(vars(close), .funs = ts_impute_vec, period = 1)
Registered S3 methods overwritten by 'forecast':
method from
fitted.fracdiff fracdiff
residuals.fracdiff fracdiff
spy_prices_hourly
Sliding / rolling calculations
Rolling functions are also useful for working with time series data. For example, we might want to calculate a rolling average of our daily portfolio returns. This can be done using the slidify() function:
roll_avg <- slidify(.f = AVERAGE, .period = 7, .align = "center", .partial = TRUE)
portfolio_returns %>%
mutate(rolling_avg = roll_avg(returns)) %>%
pivot_longer(cols = c(returns, rolling_avg)) %>%
plot_time_series(date, value, .color_var = name, .smooth = FALSE, .plotly_slider = TRUE)
LS0tCnRpdGxlOiAiVmlzdWFsaXppbmcgYW5kIGRhdGEgd3JhbmdsaW5nIHdpdGggdGhlIGB0aW1ldGtgIHBhY2thZ2UgZm9yIFIiCm91dHB1dDoKICBodG1sX25vdGVib29rOgogICAgdG9jOiB5ZXMKICAgIHRvY19mbG9hdDogeWVzCi0tLQoKYGBge3Igc2V0dXB9CmxpYnJhcnkodGlkeXZlcnNlKQpsaWJyYXJ5KHRpZHltb2RlbHMpCmxpYnJhcnkodGlkeXF1YW50KQoKIyByZW1vdGVzOjppbnN0YWxsX2dpdGh1YigiYnVzaW5lc3Mtc2NpZW5jZS90aW1ldGsiKQpsaWJyYXJ5KHRpbWV0aykKYGBgCgojIyBMb2FkaW5nIFN0b2NrIERhdGEgd2l0aCBgdGlkeXF1YW50YAoKRm9yIG91ciBkYXRhLCB3ZSdsbCBjcmVhdGUgYSBwb3J0Zm9saW8gb2YgNSBFVEZzOiBTUFksIEVGQSwgSUpTLCBFRU0sIGFuZCBBR0cuCgpGaXJzdCwgd2UnbGwgbG9hZCB0aGUgZGF0YSBmcm9tIFlhaG9vIEZpbmFuY2UgdXNpbmcgYHRxX2dldCgpYCBhbmQgd2lsbCBjYWxjdWxhdGUgZGFpbHkgcmV0dXJucyB1c2luZyBgdHFfdHJhbnNtdXRlKClgLiAKCmBgYHtyIGRhdGF9CiMgVGhlIHN5bWJvbHMgdmVjdG9yIGhvbGRzIG91ciB0aWNrZXJzLiAKc3ltYm9scyA8LSBjKCJTUFkiLCJFRkEiLCAiSUpTIiwgIkVFTSIsIkFHRyIpCgpldGZfZGFpbHlfcmV0dXJucyA8LSAKICBzeW1ib2xzICU+JSAKICB0cV9nZXQoZ2V0ID0gInN0b2NrLnByaWNlcyIsIGZyb20gPSAiMjAwNC0wMS0wMSIpICU+JSAKICBncm91cF9ieShzeW1ib2wpICU+JSAKICB0cV90cmFuc211dGUoYWRqdXN0ZWQsIHBlcmlvZFJldHVybiwgcGVyaW9kID0gImRhaWx5IiwgY29sX3JlbmFtZSA9ICJyZXR1cm5zIikKYGBgCgoKTmV4dCwgd2UnbGwgYnVpbGQgYSBwb3J0Zm9saW8gd2l0aCB0aGUgZm9sbG93aW5nIHdlaWdodHM6CgoqIFNQWTogMjUlCiogRUZBIDI1JQoqIElKUyAyNSUKKiBFRU0gMjAlCiogQUdHIDUlCgpgYGB7ciBBZ2dyZXNzaXZlIFBvcnRmb2xpb30Kd19hZ2cgPC0gYygwLjI1LCAwLjI1LCAwLjI1LCAwLjIwLCAwLjA1KQoKcG9ydGZvbGlvX3JldHVybnMgPC0gCiAgZXRmX2RhaWx5X3JldHVybnMgJT4lIAogIHRxX3BvcnRmb2xpbyhhc3NldHNfY29sID0gc3ltYm9sLCAKICAgICAgICAgICAgICAgcmV0dXJuc19jb2wgPSByZXR1cm5zLCAKICAgICAgICAgICAgICAgd2VpZ2h0cyA9IHdfYWdnLAogICAgICAgICAgICAgICBjb2xfcmVuYW1lID0gInJldHVybnMiKQpgYGAKCgoKIyMgVmlzdWFsaXplIFRpbWUgU2VyaWVzIERhdGEKCldlIGNhbiBjcmVhdGUgYSBuaWNlIGxvb2tpbmcgcGxvdCBvZiBvdXIgdGltZSBzZXJpZXMgZGF0YSB3aXRoIGEgc2luZ2xlIGxpbmUgb2YgY29kZS4KCmBgYHtyfQpwb3J0Zm9saW9fcmV0dXJucyAlPiUKICBwbG90X3RpbWVfc2VyaWVzKGRhdGUsIHJldHVybnMsIC5jb2xvcl92YXIgPSB5ZWFyKGRhdGUpLAogICAgICAgICAgICAgICAgICAgLmludGVyYWN0aXZlID0gRkFMU0UsIC5jb2xvcl9sYWIgPSAiWWVhciIpCmBgYAoKV2UgY2FuIGV2ZW4gbWFrZSB0aGlzIHBsb3QgaW50ZXJhY3RpdmUgdXNpbmcgYHRpbWV0a2AncyBpbnRlZ3JhdGlvbiB3aXRoIGBwbG90bHlgOgoKYGBge3J9CnBvcnRmb2xpb19yZXR1cm5zICU+JQogIHBsb3RfdGltZV9zZXJpZXMoZGF0ZSwgcmV0dXJucywgLmNvbG9yX3ZhciA9IHllYXIoZGF0ZSksCiAgICAgICAgICAgICAgICAgICAuaW50ZXJhY3RpdmUgPSBUUlVFLCAucGxvdGx5X3NsaWRlciA9IFRSVUUsIC5jb2xvcl9sYWIgPSAiWWVhciIpCmBgYAoKCmB0aW1ldGtgIHdpbGwgYXV0b21hdGljYWxseSBmYWNldCBwbG90cyB3aXRoIGdyb3VwZWQgZGF0YToKCmBgYHtyfQpldGZfZGFpbHlfcmV0dXJucyAlPiUgCiAgZ3JvdXBfYnkoc3ltYm9sKSAlPiUgCiAgcGxvdF90aW1lX3NlcmllcyhkYXRlLCByZXR1cm5zLCAuZmFjZXRfbmNvbCA9IDIsIC5jb2xvcl92YXIgPSBzeW1ib2wsIC5pbnRlcmFjdGl2ZSA9IEZBTFNFKQpgYGAKCgpgdGltZXRrYCBhbHNvIGhhcyBhIGJ1aWx0LWluIGZ1bmN0aW9uIGZvciBbcGxvdHRpbmcgYW5vbWFseSBkaWFnbm9zdGljc10oaHR0cHM6Ly9idXNpbmVzcy1zY2llbmNlLmdpdGh1Yi5pby90aW1ldGsvcmVmZXJlbmNlL3Bsb3RfYW5vbWFseV9kaWFnbm9zdGljcy5odG1sKToKCmBgYHtyfQpwb3J0Zm9saW9fcmV0dXJucyAlPiUgCiAgcGxvdF9hbm9tYWx5X2RpYWdub3N0aWNzKGRhdGUsIHJldHVybnMpCmBgYAoKClRvIGFkanVzdCBvdXRsaWVyIGRldGVjdGlvbiwgY2hhbmdlIHRoZSBgYWxwaGFgIHBhcmFtZXRlci4KCkFub21hbHkgZGV0ZWN0aW9uIGlzIGRvbmUgYnkgdGhlIGB0a19hbm9tYWx5X2RpYWdub3N0aWNzYCBmdW5jdGlvbjoKCmBgYHtyfQpwb3J0Zm9saW9fcmV0dXJucyAlPiUgCiAgdGtfYW5vbWFseV9kaWFnbm9zdGljcyhkYXRlLCByZXR1cm5zKQpgYGAKCgojIyMgU2hpbnkgd2l0aCBgdGltZXRrYAoKV2UgY2FuIHVzZSB0aGVzZSB2aXN1YWxpemF0aW9uIHRvb2xzIHRvIGVhc2lseSBidWlsZCBhIHNpbXBsZSBbKipTaGlueSBhcHBsaWNhdGlvbioqXShodHRwczovL25yb2hyLnNoaW55YXBwcy5pby90aW1ldGstYXBwLykgdGhhdCBoZWxwcyB1cyBkaWFnbm9zZSB0aW1lIHNlcmllcyBkYXRhLgoKCiMjIERhdGEgV3JhbmdsaW5nCgojIyMgU3VtbWFyaXppbmcgYW5kIGZpbHRlcmluZyBieSB0aW1lCgpgdGltZXRrYCBhbHNvIGluY2x1ZGVzIHNvbWUgZnVuY3Rpb25zIHRoYXQgd29yayBsaWtlIGNvbW1vbiBgZHBseXJgIHZlcmJzLCBidXQgYXJlIGVzcGVjaWFsbHkgdXNlZnVsIHdoZW4gd29ya2luZyB3aXRoIHRpbWUgc2VyaWVzIGRhdGEuCgpMZXQncyBsb29rIGF0IG91ciBpbmRpdmlkdWFsIEVURnMgdG8gYXBwbHkgc29tZSBvZiB0aGUgZGF0YSB3cmFuZ2xpbmcgdG9vbHMgZnJvbSBgdGltZXRrYC4KCmBgYHtyfQpldGZfZGFpbHlfcmV0dXJucyAlPiUgCiAgZ3JvdXBfYnkoc3ltYm9sKSAlPiUgCiAgcGxvdF90aW1lX3NlcmllcyhkYXRlLCByZXR1cm5zLCAuZmFjZXRfbmNvbCA9IDIsIC5pbnRlcmFjdGl2ZSA9IEZBTFNFKQpgYGAKCgpXZSBjYW4gdXNlIGBzdW1tYXJpc2VfYnlfdGltZSgpYCB0byBhZ2dyZWdhdGUgb3ZlciBhIHNwZWNpZmljIHRpbWUgcGVyaW9kLiBGb3IgZXhhbXBsZSwgbGV0J3Mgc21vb3RoIG91ciByZXR1cm5zIGJ5IGF2ZXJhZ2luZyB0aGVtIG92ZXIgZWFjaCBtb250aDoKCmBgYHtyfQpldGZfZGFpbHlfcmV0dXJucyAlPiUgCiAgc3VtbWFyaXNlX2J5X3RpbWUoCiAgICBkYXRlLCAuYnkgPSAibW9udGgiLAogICAgcmV0dXJucyA9IEFWRVJBR0UocmV0dXJucykKICApICU+JSAKICBwbG90X3RpbWVfc2VyaWVzKGRhdGUsIHJldHVybnMsIC5mYWNldF9uY29sID0gMiwgLmludGVyYWN0aXZlID0gRkFMU0UpCmBgYAoKCldlIGNhbiBhbHNvIGZpbHRlciBieSB0aW1lIHJhbmdlIHVzaW5nIHRoZSBgZmlsdGVyX2J5X3RpbWUoKWAgZnVuY3Rpb246CgpgYGB7cn0KZXRmX2RhaWx5X3JldHVybnMgJT4lIAogIGZpbHRlcl9ieV90aW1lKGRhdGUsICIyMDEyLTA2IiwgIjIwMjAiKSAlPiUgCiAgc3VtbWFyaXNlX2J5X3RpbWUoCiAgICBkYXRlLCAuYnkgPSAibW9udGgiLAogICAgcmV0dXJucyA9IEFWRVJBR0UocmV0dXJucykKICApICU+JSAKICBwbG90X3RpbWVfc2VyaWVzKGRhdGUsIHJldHVybnMsIC5mYWNldF9uY29sID0gMiwgLmludGVyYWN0aXZlID0gRkFMU0UpCmBgYAoKCiMjIyBQYWRkaW5nIGFuZCBpbXB1dGluZyBtaXNzaW5nIGRhdGEKCmB0aW1ldGtgIGFsc28gaGFzIGZ1bmN0aW9ucyBmb3IgcGFkZGluZyBhbmQgaW1wdXRpbmcgbWlzc2luZyBkYXRhLgoKT3VyIHRpYmJsZXMgb2YgZGF0YSBhcmUgbWlzc2luZyBzb21lIGRhdGVzIC0gd2Vla2VuZHMgYW5kIGhvbGlkYXlzLiBJbiBvdXIgRVRGIHJldHVybnM6CgpgYGB7cn0KZXRmX2RhaWx5X3JldHVybnMKYGBgCgpTb21lIHBhY2thZ2VzIGZvciB3b3JraW5nIHdpdGggdGltZSBzZXJpZXMgZGF0YSByZXF1aXJlIHRoZXJlIHRvIGJlIG5vIG1pc3NpbmcgZW50cmllcy4gTHVja2lseSwgYHRpbWV0a2AgbWFrZXMgaXQgZWFzeSB0byBhZGQgcGxhY2Vob2xkZXJzIGZvciBhbnkgbXNpc2luZyBkYXRlcyBpbiBvdXIgZGF0YS4KClRoZSBgcGFkX2J5X3RpbWUoKWAgZnVuY3Rpb24gd2lsbCByZXBsYWNlIG1pc3NpbmcgdmFsdWVzIHdpdGggYE5BYCBieSBkZWZhdWx0LgoKYGBge3J9CmV0Zl9kYWlseV9yZXR1cm5zICU+JSAKICBwYWRfYnlfdGltZShkYXRlLCAuYnkgPSAiYXV0byIpCmBgYAoKCkluIHNvbWUgY2FzZXMsIHdlIG1heSB3YW50IHRvIGltcHV0ZSBtaXNzaW5nIHZhbHVlcyAoaS5lLiwgcmVwbGFjZSBgTkFgIHdpdGggYW4gYWN0dWFsIHZhbHVlKS4gVGhpcyBhbHNvIGFsbG93cyB1cyB0byBpbXB1dGUgbW9yZSBncmFudWxhciBkYXRhLCBsaWtlIGhvdXJseSBzdG9jayBwcmljZXMuCgpMZXQncyBzdGFydCB3aXRoIHRoZSBkYWlseSBjbG9zaW5nIHByaWNlcyBmb3IgU1BZOgoKYGBge3J9CnNweV9wcmljZXMgPC0gCiAgdHFfZ2V0KCJTUFkiLCBnZXQgPSAic3RvY2sucHJpY2VzIiwgZnJvbSA9ICIyMDA0LTAxLTAxIikgCgpzcHlfcHJpY2VzCgpzcHlfcHJpY2VzICU+JSAKICBwbG90X3RpbWVfc2VyaWVzKGRhdGUsIGNsb3NlLCAuaW50ZXJhY3RpdmUgPSBGQUxTRSkKYGBgCgoKTm93IGxldCdzIGltcHV0ZSBob3VybHkgY2xvc2luZyBwcmljZXMgZm9yIGV2ZXJ5IGRheSBvZiB0aGUgeWVhciB1c2luZyBgdHNfaW1wdXRlX3ZlYygpYC4gKFRoaXMgaXMgZG9uZSBsaW5lYXJseSB3aGVuIGBwZXJpb2QgPSAxYCkKCmBgYHtyfQpzcHlfcHJpY2VzX2hvdXJseSA8LSAKICBzcHlfcHJpY2VzICU+JSAKICBwYWRfYnlfdGltZShkYXRlLCAuYnkgPSAiaG91ciIpICU+JSAjIHBhZCB3aXRoIGFkZGl0aW9uYWwgb2JzZXJ2YXRpb25zIGZvciBldmVyeSBob3VyCiAgbXV0YXRlX2F0KHZhcnMoY2xvc2UpLCAuZnVucyA9IHRzX2ltcHV0ZV92ZWMsIHBlcmlvZCA9IDEpCgpzcHlfcHJpY2VzX2hvdXJseQpgYGAKCgojIyMgU2xpZGluZyAvIHJvbGxpbmcgY2FsY3VsYXRpb25zCgpSb2xsaW5nIGZ1bmN0aW9ucyBhcmUgYWxzbyB1c2VmdWwgZm9yIHdvcmtpbmcgd2l0aCB0aW1lIHNlcmllcyBkYXRhLiBGb3IgZXhhbXBsZSwgd2UgbWlnaHQgd2FudCB0byBjYWxjdWxhdGUgYSByb2xsaW5nIGF2ZXJhZ2Ugb2Ygb3VyIGRhaWx5IHBvcnRmb2xpbyByZXR1cm5zLiBUaGlzIGNhbiBiZSBkb25lIHVzaW5nIHRoZSBgc2xpZGlmeSgpYCBmdW5jdGlvbjoKCmBgYHtyfQpyb2xsX2F2ZyA8LSBzbGlkaWZ5KC5mID0gQVZFUkFHRSwgLnBlcmlvZCA9IDcsIC5hbGlnbiA9ICJjZW50ZXIiLCAucGFydGlhbCA9IFRSVUUpCgpwb3J0Zm9saW9fcmV0dXJucyAlPiUgCiAgbXV0YXRlKHJvbGxpbmdfYXZnID0gcm9sbF9hdmcocmV0dXJucykpICU+JQogIHBpdm90X2xvbmdlcihjb2xzID0gYyhyZXR1cm5zLCByb2xsaW5nX2F2ZykpICU+JSAKICBwbG90X3RpbWVfc2VyaWVzKGRhdGUsIHZhbHVlLCAuY29sb3JfdmFyID0gbmFtZSwgLnNtb290aCA9IEZBTFNFLCAucGxvdGx5X3NsaWRlciA9IFRSVUUpCmBgYAoKCg==